/*
 * Copyright � 2002-2011 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace Spring.Objects.Factory.Config;

/// <summary>
/// <see cref="Spring.Objects.Factory.IFactoryObject"/> implementation that
/// evaluates a property path on a given target object.
/// </summary>
/// <remarks>
/// <p>
/// The target object can be specified directly or via an object name (see
/// example below).
/// </p>
/// <p>
/// Please note that the <see cref="Spring.Objects.Factory.Config.PropertyPathFactoryObject"/>
/// is an <see cref="Spring.Objects.Factory.IFactoryObject"/> implementation, and as such has
/// to comply with the contract of the <see cref="Spring.Objects.Factory.IFactoryObject"/>
/// interface; more specifically, this means that the end result of the property lookup path
/// evaluation cannot be <see lang="null"/> (<see cref="Spring.Objects.Factory.IFactoryObject"/>
/// implementations are not permitted to return <see lang="null"/>). If the resut of a
/// property lookup path evaluates to <see lang="null"/>, an exception will be thrown.
/// </p>
/// </remarks>
/// <example>
/// <code escaped="true">
/// <!-- this is the target object -->
/// <object id="foo" type="Whatever.MyClass, MyAssembly" singleton="false">
///		<!-- a System.String typed property -->
///		<property name="name" value="Chinua Achebe"/>
/// </object>
///
/// <!--
///		will result in "Chinua Achebe", which is the value of the 'name' property of the 'foo' object
///	-->
/// <object id="consumer"
///			type="Spring.Objects.Factory.Config.PropertyPathFactoryObject, Spring.Core">
///		<property name="targetObject" ref="foo"/>
///		<property name="propertyPath" value="name"/>
/// </object>
///
/// <!--
///		will result in "Chinua Achebe", which is the value of the 'name' property of the 'foo' object
///	-->
/// <object id="foo.name"
///			type="Spring.Objects.Factory.Config.PropertyPathFactoryObject, Spring.Core"/>
///
/// <!--
///		will result in "Chinua Achebe", which is the value of the 'name' property of the 'foo' object
/// -->
/// <object id="consumer"
///			type="Spring.Objects.Factory.Config.PropertyPathFactoryObject, Spring.Core">
///		<property name="targetObjectName" value="foo"/>
///		<property name="propertyPath" value="name"/>
/// </object>
///
/// <!--
///		will result in (the int value) '13', which is the value of the length property of the 'name'
///		property of the 'foo' object.
///
///		in this case, the first 'part' of the id is taken to be the name of the target object ('foo');
///		the remainder of the name is taken to be the property lookup path
///	-->
/// <object id="foo.name.length"
///			type="Spring.Objects.Factory.Config.PropertyPathFactoryObject, Spring.Core"/>
/// </code>
/// </example>
/// <author>Juergen Hoeller</author>
/// <author>Rick Evans (.NET)</author>
[Serializable]
public class PropertyPathFactoryObject : IFactoryObject, IObjectNameAware, IObjectFactoryAware
{
    private IObjectWrapper targetObjectWrapper;
    private string targetObjectName;
    private string propertyPath;
    private Type resultType;
    private string objectName;
    private IObjectFactory objectFactory;

    /// <summary>
    /// The target object that the property path lookup is to be applied to.
    /// </summary>
    /// <remarks>
    /// <p>
    /// This would most likely be an inner object, but can of course be
    /// any object reference.
    /// </p>
    /// </remarks>
    /// <value>
    /// The target object that the property path lookup is to be applied to.
    /// </value>
    /// <seealso cref="Spring.Objects.Factory.Config.PropertyPathFactoryObject.TargetObjectName"/>
    public object TargetObject
    {
        set { this.targetObjectWrapper = new ObjectWrapper(value); }
    }

    /// <summary>
    /// The (object) name of the target object that the property path lookup
    /// is to be applied to.
    /// </summary>
    /// <remarks>
    /// <p>
    /// Please note that any leading or trailing whitespace <b>will</b> be
    /// trimmed from this name prior to resolution. The implication of this is that
    /// one cannot use the <see cref="Spring.Objects.Factory.Config.PropertyPathFactoryObject"/>
    /// class in conjunction with object names that start or end with whitespace.
    /// </p>
    /// </remarks>
    /// <value>
    /// The (object) name of the target object that the property path lookup
    /// is to be applied to.
    /// </value>
    /// <seealso cref="Spring.Objects.Factory.Config.PropertyPathFactoryObject.TargetObject"/>
    public string TargetObjectName
    {
        set
        {
            if (value != null)
            {
                value = value.Trim();
            }

            this.targetObjectName = value;
        }
    }

    /// <summary>
    /// The property (lookup) path to be applied to the target object.
    /// </summary>
    /// <remarks>
    /// <p>
    /// Please note that any leading or trailing whitespace <b>will</b> be
    /// trimmed from this path prior to resolution. Whitespace is not a valid
    /// identifier for property names (in part or whole) in CLS-based languages,
    /// so this is a not unreasonable action. Please also note that whitespace
    /// that is embedded within the property path will be left as-is (which may
    /// or may not result in an error being thrown, depending on the context of
    /// the whitespace).
    /// </p>
    /// </remarks>
    /// <example>
    /// <p>
    /// Examples of such property lookup paths can be seen below; note that
    /// property lookup paths can be nested to an arbitrary level.
    /// </p>
    /// <code escaped="true">
    /// name.length
    /// accountManager.account['the key'].name
    /// accounts[0].name
    /// </code>
    /// </example>
    /// <value>
    /// The property (lookup) path to be applied to the target object.
    /// </value>
    public string PropertyPath
    {
        set
        {
            if (value != null)
            {
                value = value.Trim();
            }

            this.propertyPath = value;
        }
    }

    /// <summary>
    /// The 'expected' <see cref="System.Type"/> of the result from evaluating the
    /// property path.
    /// </summary>
    /// <remarks>
    /// <p>
    /// This is not necessary for directly specified target objects, or
    /// singleton target objects, where the <see cref="System.Type"/> can
    /// be determined via reflection. Just specify this in case of a
    /// prototype target, provided that you need matching by type (for
    /// example, for autowiring).
    /// </p>
    /// <p>
    /// It is permissable to set the value of this property to
    /// <see lang="null"/> (which in any case is the default value).
    /// </p>
    /// </remarks>
    /// <value>
    /// The 'expected' <see cref="System.Type"/> of the result from evaluating the
    /// property path.
    /// </value>
    public Type ResultType
    {
        set { this.resultType = value; }
    }

    /// <summary>
    /// Return an instance (possibly shared or independent) of the object
    /// managed by this factory.
    /// </summary>
    /// <returns>
    /// An instance (possibly shared or independent) of the object managed by
    /// this factory.
    /// </returns>
    /// <see cref="Spring.Objects.Factory.IFactoryObject.GetObject()"/>
    public object GetObject()
    {
        IObjectWrapper target = this.targetObjectWrapper;
        if (target == null)
        {
            // fetch the prototype object object...
            target = new ObjectWrapper(this.objectFactory[this.targetObjectName]);
        }

        object value = target.GetPropertyValue(this.propertyPath);
        if (value == null)
        {
            throw new FatalObjectException("PropertyPathFactoryObject is not allowed to return null, " +
                                           "but property value for path '" + this.propertyPath + "' is null.");
        }

        return value;
    }

    /// <summary>
    /// Return the <see cref="System.Type"/> of object that this
    /// <see cref="Spring.Objects.Factory.IFactoryObject"/> creates, or
    /// <see langword="null"/> if not known in advance.
    /// </summary>
    /// <see cref="Spring.Objects.Factory.IFactoryObject.ObjectType"/>
    public Type ObjectType
    {
        get { return this.resultType; }
    }

    /// <summary>
    /// Is the object managed by this factory a singleton or a prototype?
    /// </summary>
    /// <see cref="Spring.Objects.Factory.IFactoryObject.IsSingleton"/>
    public bool IsSingleton
    {
        get { return false; }
    }

    /// <summary>
    /// Set the name of the object in the object factory that created this object.
    /// </summary>
    /// <remarks>
    /// <p>
    /// The object name of this
    /// <see cref="Spring.Objects.Factory.Config.PropertyPathFactoryObject"/>
    /// will be interpreted as "objectName.property" pattern, if neither the
    /// <see cref="Spring.Objects.Factory.Config.PropertyPathFactoryObject.TargetObjectName"/>
    /// <see cref="Spring.Objects.Factory.Config.PropertyPathFactoryObject.TargetObject"/>
    /// have been supplied (set).
    /// </p>
    /// <p>
    /// This allows for concise object definitions with just an id or name.
    /// </p>
    /// </remarks>
    /// <value>
    /// The name of the object in the factory.
    /// </value>
    public string ObjectName
    {
        set { this.objectName = value; }
    }

    /// <summary>
    /// Callback that supplies the owning factory to an object instance.
    /// </summary>
    /// <value>
    /// Owning <see cref="IInitializingObject.AfterPropertiesSet"/>
    /// (may not be <see langword="null"/>). The object can immediately
    /// call methods on the factory.
    /// </value>
    /// <exception cref="IInitializingObject">
    /// In case of initialization errors.
    /// </exception>
    public IObjectFactory ObjectFactory
    {
        set
        {
            this.objectFactory = value;
            if (this.targetObjectWrapper != null && this.targetObjectName != null)
            {
                throw new ArgumentException("Only one of the TargetObjectName or TargetObject properties can be set, not both.");
            }

            if (this.targetObjectWrapper == null && this.targetObjectName == null)
            {
                if (this.propertyPath != null)
                {
                    throw new ArgumentException(
                        "Specify TargetObject or TargetObjectName property in combination with PropertyPath.");
                }

                // no other properties specified: check object name...
                string strippedObjectname = this.objectName.Trim();
                int dotIndex = strippedObjectname.IndexOf('.');
                if (dotIndex <= 0)
                {
                    throw new ArgumentException(
                        "Neither TargetObject nor TargetObjectName specified, and PropertyPathFactoryObject " +
                        "object name '" + this.objectName + "' does not follow 'objectName.property' syntax.");
                }

                this.targetObjectName = strippedObjectname.Substring(0, dotIndex);
                this.propertyPath = strippedObjectname.Substring(dotIndex + 1);
            }
            else if (this.propertyPath == null)
            {
                throw new ArgumentException("The 'PropertyPath' property has not been set.");
            }

            if (this.targetObjectWrapper == null
                && this.objectFactory.IsSingleton(this.targetObjectName))
            {
                // eagerly fetch singleton target object, and determine result type...
                this.targetObjectWrapper
                    = new ObjectWrapper(this.objectFactory.GetObject(this.targetObjectName));
                this.resultType = this.targetObjectWrapper.GetPropertyType(this.propertyPath);
            }
        }
    }
}
